#pragma once

#include "quickjs.h"

#include <algorithm>
#include <cassert>
#include <cstddef>
#include <cstdio>
#include <expected>
#include <filesystem>
#include <fstream>
#include <functional>
#include <future>
#include <ios>
#include <iostream>
#include <memory>
#include <mutex>
#include <optional>
#include <print>
#include <ranges>
#include <sstream>
#include <stdexcept>
#include <string>
#include <string_view>
#include <tuple>
#include <type_traits>
#include <unordered_map>
#include <variant>
#include <vector>

#include "Windows.h"
extern thread_local bool is_thread_js_main;
#if defined(__cpp_rtti)
#define QJSPP_TYPENAME(...) (typeid(__VA_ARGS__).name())
#else
#define QJSPP_TYPENAME(...) #__VA_ARGS__
#endif

namespace qjs {
class Context;
class Value;
inline void setCurrentContext(JSContext *);
inline JSContext *getContextFromWrapped(Context *);
inline std::weak_ptr<Context> weakFromContext(JSContext *);

void wait_with_msgloop(std::function<void()> f);
/** Exception type.
 * Indicates that exception has occured in JS context.
 */
class exception : public std::runtime_error {
    JSContext *ctx;
    inline std::string message(JSContext *ctx = nullptr) const;

public:
    exception(JSContext *ctx) : std::runtime_error(message(ctx)), ctx(ctx) {}
    Context &context() const;

    /// Clears and returns the occurred exception.
    Value get();
};

/** std::shared_ptr, for compatibility with quickjspp v2. */
template <class T> using shared_ptr = std::shared_ptr<T>;
/** std::make_shared, for compatibility with quickjspp v2. */
template <class T, typename... Args>
shared_ptr<T> make_shared(JSContext *, Args &&...args) {
    return std::make_shared<T>(std::forward<Args>(args)...);
}

/** Javascript conversion traits.
 * Describes how to convert type R to/from JSValue. Second template argument can
 * be used for SFINAE/enable_if type filters.
 */
template <typename R, typename /*_SFINAE*/ = void> struct js_traits {
    /** Create an object of C++ type R given JSValue v and JSContext.
     * This function is intentionally not implemented. User should implement
     * this function for their own type.
     * @param v This value is passed as JSValueConst so it should be freed by
     * the caller.
     * @throws exception in case of conversion error
     */
    static R unwrap(JSContext *ctx, JSValueConst v) = delete;

    /** Create JSValue from an object of type R and JSContext.
     * This function is intentionally not implemented. User should implement
     * this function for their own type.
     * @return Returns JSValue which should be freed by the caller or
     * JS_EXCEPTION in case of error.
     */
    static JSValue wrap(JSContext *ctx, R value) = delete;
};

/** Conversion traits for JSValue (identity).
 */
template <> struct js_traits<JSValue> {
    static JSValue unwrap(JSContext *ctx, JSValueConst v) noexcept {
        return JS_DupValue(ctx, v);
    }

    static JSValue wrap(JSContext *ctx, JSValue &&v) noexcept { return v; }
};

/** Conversion traits for integers.
 */
template <typename Int>
struct js_traits<Int, std::enable_if_t<std::is_integral_v<Int>>> {
    /// @throws exception
    static Int unwrap(JSContext *ctx, JSValueConst v) {
        if constexpr (sizeof(Int) > sizeof(int32_t)) {
            int64_t r;
            if (JS_ToInt64(ctx, &r, v))
                throw exception{ctx};
            return static_cast<Int>(r);
        } else {
            int32_t r;
            if (JS_ToInt32(ctx, &r, v))
                throw exception{ctx};
            return static_cast<Int>(r);
        }
    }

    static JSValue wrap(JSContext *ctx, Int i) noexcept {
        if constexpr (std::is_same_v<Int, uint32_t> ||
                      sizeof(Int) > sizeof(int32_t))
            return JS_NewInt64(ctx, static_cast<Int>(i));
        else
            return JS_NewInt32(ctx, static_cast<Int>(i));
    }
};

/** Conversion traits for boolean.
 */
template <> struct js_traits<bool> {
    static bool unwrap(JSContext *ctx, JSValueConst v) noexcept {
        // TODO: is this behaviour correct?
        return JS_ToBool(ctx, v) > 0;
    }

    static JSValue wrap(JSContext *ctx, bool i) noexcept {
        return JS_NewBool(ctx, i);
    }
};

/** Conversion trait for void.
 */
template <> struct js_traits<void> {
    /// @throws exception if jsvalue is neither undefined nor null
    static void unwrap(JSContext *ctx, JSValueConst value) {
        if (JS_IsException(value))
            throw exception{ctx};
    }
};

/** Conversion traits for float64/double.
 */
template <> struct js_traits<double> {
    /// @throws exception
    static double unwrap(JSContext *ctx, JSValueConst v) {
        double r;
        if (JS_ToFloat64(ctx, &r, v))
            throw exception{ctx};
        return r;
    }

    static JSValue wrap(JSContext *ctx, double i) noexcept {
        return JS_NewFloat64(ctx, i);
    }
};

template <> struct js_traits<float> {
    /// @throws exception
    static float unwrap(JSContext *ctx, JSValueConst v) {
        double r;
        if (JS_ToFloat64(ctx, &r, v))
            throw exception{ctx};
        return static_cast<float>(r);
    }

    static JSValue wrap(JSContext *ctx, float i) noexcept {
        return JS_NewFloat64(ctx, static_cast<double>(i));
    }
};

namespace detail {
/** Fake std::string_view which frees the string on destruction.
 */
class js_string : public std::string_view {
    using Base = std::string_view;
    JSContext *ctx = nullptr;

    friend struct js_traits<std::string_view>;

    js_string(JSContext *ctx, const char *ptr, std::size_t len)
        : Base(ptr, len), ctx(ctx) {}

public:
    template <typename... Args>
    js_string(Args &&...args)
        : Base(std::forward<Args>(args)...), ctx(nullptr) {}

    js_string(const js_string &other) = delete;

    operator const char *() const { return this->data(); }

    ~js_string() {
        if (ctx)
            JS_FreeCString(ctx, this->data());
    }
};
} // namespace detail

/** Conversion traits from std::string_view and to detail::js_string. */
template <> struct js_traits<std::string_view> {
    static detail::js_string unwrap(JSContext *ctx, JSValueConst v) {
        size_t plen;
        const char *ptr = JS_ToCStringLen(ctx, &plen, v);
        if (!ptr)
            throw exception{ctx};
        return detail::js_string{ctx, ptr, plen};
    }

    static JSValue wrap(JSContext *ctx, std::string_view str) noexcept {
        return JS_NewStringLen(ctx, str.data(), str.size());
    }
};

/** Conversion traits for std::string */
template <> // slower
struct js_traits<std::string> {
    static std::string unwrap(JSContext *ctx, JSValueConst v) {
        auto str_view = js_traits<std::string_view>::unwrap(ctx, v);
        return std::string{str_view.data(), str_view.size()};
    }

    static JSValue wrap(JSContext *ctx, const std::string &str) noexcept {
        return JS_NewStringLen(ctx, str.data(), str.size());
    }
};

/** Conversion from const char * */
template <> struct js_traits<const char *> {
    static JSValue wrap(JSContext *ctx, const char *str) noexcept {
        return JS_NewString(ctx, str);
    }

    static detail::js_string unwrap(JSContext *ctx, JSValueConst v) {
        return js_traits<std::string_view>::unwrap(ctx, v);
    }
};

template <typename T>
concept has_user_defined_conversion = requires {
    { js_traits<T>::unwrap };
};

template <typename T>
concept is_callable_concept = requires(T t) {
    { &T::operator() };
};

/** Conversion from const std::variant */
template <typename... Ts> struct js_traits<std::variant<Ts...>> {
    static JSValue wrap(JSContext *ctx, std::variant<Ts...> value) noexcept {
        return std::visit(
            [ctx](auto &&value) {
                using T = std::decay_t<decltype(value)>;
                return js_traits<T>::wrap(ctx, value);
            },
            std::move(value));
    }

    /* Useful type traits */
    template <typename T> struct is_shared_ptr : std::false_type {};
    template <typename T>
    struct is_shared_ptr<std::shared_ptr<T>> : std::true_type {};
    template <typename T> struct is_string {
        static constexpr bool value =
            std::is_same_v<T, const char *> ||
            std::is_same_v<std::decay_t<T>, std::string> ||
            std::is_same_v<std::decay_t<T>, std::string_view>;
    };
    template <typename T> struct is_boolean {
        static constexpr bool value = std::is_same_v<std::decay_t<T>, bool>;
    };
    template <typename T> struct is_double {
        static constexpr bool value = std::is_same_v<std::decay_t<T>, double>;
    };
    template <typename T> struct is_vector : std::false_type {};
    template <typename T> struct is_vector<std::vector<T>> : std::true_type {};
    template <typename T> struct is_pair : std::false_type {};
    template <typename U, typename V>
    struct is_pair<std::pair<U, V>> : std::true_type {};
    template <typename T> struct is_variant : std::false_type {};
    template <typename... Us>
    struct is_variant<std::variant<Us...>> : std::true_type {};

    template <typename T> struct is_callable {
        static constexpr bool value = is_callable_concept<T>;
    };

    /** Attempt to match common types (integral, floating-point, string, etc.)
     */
    template <template <typename R> typename Trait, typename U, typename... Us>
    static std::optional<std::variant<Ts...>> unwrapImpl(JSContext *ctx,
                                                         JSValueConst v) {
        if constexpr (Trait<U>::value) {
            return js_traits<U>::unwrap(ctx, v);
        }
        if constexpr ((sizeof...(Us)) > 0) {
            return unwrapImpl<Trait, Us...>(ctx, v);
        }
        return std::nullopt;
    }

    /** Attempt to match class ID with type */
    template <typename U, typename... Us>
    static std::optional<std::variant<Ts...>>
    unwrapObj(JSContext *ctx, JSValueConst v, JSClassID class_id) {
        if constexpr (is_shared_ptr<U>::value) {
            if (class_id == js_traits<U>::QJSClassId) {
                return js_traits<U>::unwrap(ctx, v);
            }
        }

        // try to unwrap embedded variant (variant<variant<...>>), might be slow
        if constexpr (is_variant<U>::value) {
            if (auto opt = js_traits<std::optional<U>>::unwrap(ctx, v))
                return *opt;
        }

        if constexpr (is_vector<U>::value) {
            if (JS_IsArray(v) == 1) {
                auto firstElement = JS_GetPropertyUint32(ctx, v, 0);
                bool ok = isCompatible<std::decay_t<typename U::value_type>>(
                    ctx, firstElement);
                JS_FreeValue(ctx, firstElement);
                if (ok) {
                    return U{js_traits<U>::unwrap(ctx, v)};
                }
            }
        }

        if constexpr (is_pair<U>::value) {
            if (JS_IsArray(v) == 1) {
                // todo: check length?
                auto firstElement = JS_GetPropertyUint32(ctx, v, 0);
                auto secondElement = JS_GetPropertyUint32(ctx, v, 1);
                bool ok = isCompatible<std::decay_t<typename U::first_type>>(
                              ctx, firstElement) &&
                          isCompatible<std::decay_t<typename U::second_type>>(
                              ctx, secondElement);
                JS_FreeValue(ctx, firstElement);
                JS_FreeValue(ctx, secondElement);
                if (ok) {
                    return U{js_traits<U>::unwrap(ctx, v)};
                }
            }
        }

        if constexpr ((sizeof...(Us)) > 0) {
            return unwrapObj<Us...>(ctx, v, class_id);
        }
        return std::nullopt;
    }

    /** Attempt to cast to types satisfying traits, ordered in terms of priority
     */
    template <template <typename T> typename Trait,
              template <typename T> typename... Traits>
    static std::variant<Ts...> unwrapPriority(JSContext *ctx, JSValueConst v) {
        if (auto result = unwrapImpl<Trait, Ts...>(ctx, v)) {
            return *result;
        }
        if constexpr ((sizeof...(Traits)) > 0) {
            return unwrapPriority<Traits...>(ctx, v);
        }
        JS_ThrowTypeError(ctx, "Expected type %s",
                          QJSPP_TYPENAME(std::variant<Ts...>));
        throw exception{ctx};
    }

    template <typename T>
    static bool isCompatible(JSContext *ctx, JSValueConst v) noexcept {
        // const char * type_name = typeid(T).name();
        if (JS_IsFunction(ctx, v)) {
            return is_callable<T>::value;
        }
        switch (JS_VALUE_GET_TAG(v)) {
        case JS_TAG_STRING:
            return is_string<T>::value;

        case JS_TAG_FUNCTION_BYTECODE:
            return is_callable<T>::value;
        case JS_TAG_OBJECT:
            if (JS_IsArray(v) == 1)
                return is_vector<T>::value || is_pair<T>::value;
            if constexpr (is_shared_ptr<T>::value) {
                if (JS_GetClassID(v) == js_traits<T>::QJSClassId)
                    return true;
            }
            return false;

        case JS_TAG_INT:
            [[fallthrough]];
        case JS_TAG_BIG_INT:
            return std::is_integral_v<T> || std::is_floating_point_v<T>;
        case JS_TAG_BOOL:
            return is_boolean<T>::value || std::is_integral_v<T> ||
                   std::is_floating_point_v<T>;

        // case JS_TAG_BIG_DECIMAL:
        //     [[fallthrough]];
        // case JS_TAG_BIG_FLOAT:
        //     [[fallthrough]];
        case JS_TAG_FLOAT64:
        default: // >JS_TAG_FLOAT64 (JS_NAN_BOXING)
            return is_double<T>::value || std::is_floating_point_v<T>;

        case JS_TAG_SYMBOL:
            [[fallthrough]];
        case JS_TAG_MODULE:
            [[fallthrough]];
        case JS_TAG_NULL:
            [[fallthrough]];
        case JS_TAG_UNDEFINED:
            [[fallthrough]];
        case JS_TAG_UNINITIALIZED:
            [[fallthrough]];
        case JS_TAG_CATCH_OFFSET:
            [[fallthrough]];
        case JS_TAG_EXCEPTION:
            break;
        }
        return false;
    }

    static std::variant<Ts...> unwrap(JSContext *ctx, JSValueConst v) {
        const auto tag = JS_VALUE_GET_TAG(v);

        if (JS_IsFunction(ctx, v)) {
            return unwrapPriority<is_callable>(ctx, v);
        }

        switch (tag) {
        case JS_TAG_STRING:
            return unwrapPriority<is_string>(ctx, v);

        case JS_TAG_FUNCTION_BYTECODE:
            return unwrapPriority<is_callable>(ctx, v);
        case JS_TAG_OBJECT:
            if (auto result = unwrapObj<Ts...>(ctx, v, JS_GetClassID(v))) {
                return *result;
            }
            JS_ThrowTypeError(
                ctx, "Expected type %s, got object with classid %d",
                QJSPP_TYPENAME(std::variant<Ts...>), JS_GetClassID(v));
            break;

        case JS_TAG_INT:
            [[fallthrough]];
        case JS_TAG_BIG_INT:
            return unwrapPriority<std::is_integral, std::is_floating_point>(ctx,
                                                                            v);
        case JS_TAG_BOOL:
            return unwrapPriority<is_boolean, std::is_integral,
                                  std::is_floating_point>(ctx, v);

        case JS_TAG_SYMBOL:
            [[fallthrough]];
        case JS_TAG_MODULE:
            [[fallthrough]];
        case JS_TAG_NULL:
            [[fallthrough]];
        case JS_TAG_UNDEFINED:
            [[fallthrough]];
        case JS_TAG_UNINITIALIZED:
            [[fallthrough]];
        case JS_TAG_CATCH_OFFSET:
            JS_ThrowTypeError(ctx, "Expected type %s, got tag %d",
                              QJSPP_TYPENAME(std::variant<Ts...>), tag);
            [[fallthrough]];
        case JS_TAG_EXCEPTION:
            break;

            // case JS_TAG_BIG_DECIMAL:
            //     [[fallthrough]];
            // case JS_TAG_BIG_FLOAT:
            //     [[fallthrough]];

        case JS_TAG_FLOAT64:
            [[fallthrough]];
        default: // more than JS_TAG_FLOAT64 (nan boxing)
            return unwrapPriority<is_double, std::is_floating_point>(ctx, v);
        }

        throw exception{ctx};
    }
};

template <typename T>
struct js_traits<std::weak_ptr<T>> : js_traits<std::shared_ptr<T>> {
    static JSValue wrap(JSContext *ctx,
                        const std::weak_ptr<T> &value) noexcept {
        return js_traits<std::shared_ptr<T>>::wrap(ctx, value.lock());
    }
};

template <typename T> struct rest : std::vector<T> {
    using std::vector<T>::vector;
    using std::vector<T>::operator=;
};

namespace detail {

/** Helper function to convert and then free JSValue. */
template <typename T> T unwrap_free(JSContext *ctx, JSValue val) {
    if constexpr (std::is_same_v<T, void>) {
        JS_FreeValue(ctx, val);
        return js_traits<T>::unwrap(ctx, val);
    } else {
        try {
            T result = js_traits<std::decay_t<T>>::unwrap(ctx, val);
            JS_FreeValue(ctx, val);
            return result;
        } catch (...) {
            JS_FreeValue(ctx, val);
            throw;
        }
    }
}

template <typename T, size_t I, size_t NArgs> struct unwrap_arg_impl {
    static auto unwrap(JSContext *ctx, int argc, JSValueConst *argv) {
        if (size_t(argc) <= I) {
            JS_ThrowTypeError(ctx,
                              "Expected at least %lu arguments but received %d",
                              (unsigned long)NArgs, argc);
            throw exception{ctx};
        }
        return js_traits<std::decay_t<T>>::unwrap(ctx, argv[I]);
    }
};

template <typename T, size_t I, size_t NArgs>
struct unwrap_arg_impl<rest<T>, I, NArgs> {
    static rest<T> unwrap(JSContext *ctx, int argc, JSValueConst *argv) {
        static_assert(
            I == NArgs - 1,
            "The `rest` argument must be the last function argument.");
        rest<T> result;
        result.reserve(argc - I);
        for (size_t i = I; i < size_t(argc); ++i)
            result.push_back(js_traits<T>::unwrap(ctx, argv[i]));
        return result;
    }
};

template <class Tuple, std::size_t... I>
Tuple unwrap_args_impl(JSContext *ctx, int argc, JSValueConst *argv,
                       std::index_sequence<I...>) {
    return Tuple{unwrap_arg_impl<std::tuple_element_t<I, Tuple>, I,
                                 sizeof...(I)>::unwrap(ctx, argc, argv)...};
}

/** Helper function to convert an array of JSValues to a tuple.
 * @tparam Args C++ types of the argv array
 */
template <typename... Args>
std::tuple<std::decay_t<Args>...> unwrap_args(JSContext *ctx, int argc,
                                              JSValueConst *argv) {
    return unwrap_args_impl<std::tuple<std::decay_t<Args>...>>(
        ctx, argc, argv, std::make_index_sequence<sizeof...(Args)>());
}

/** Helper function to call f with an array of JSValues.
 * @tparam R return type of f
 * @tparam Args argument types of f
 * @tparam Callable type of f (inferred)
 * @param ctx JSContext
 * @param f callable object
 * @param argv array of JSValue's
 * @return converted return value of f or JS_NULL if f returns void
 */
template <typename R, typename... Args, typename Callable>
JSValue wrap_call(JSContext *ctx, Callable &&f, int argc,
                  JSValueConst *argv) noexcept {
    try {
        setCurrentContext(ctx);
        if constexpr (std::is_same_v<R, void>) {
            std::apply(std::forward<Callable>(f),
                       unwrap_args<Args...>(ctx, argc, argv));
            return JS_NULL;
        } else {
            return js_traits<std::decay_t<R>>::wrap(
                ctx, std::apply(std::forward<Callable>(f),
                                unwrap_args<Args...>(ctx, argc, argv)));
        }
    } catch (exception) {
        return JS_EXCEPTION;
    } catch (std::exception const &err) {
        JS_ThrowInternalError(ctx, "%s", err.what());
        return JS_EXCEPTION;
    } catch (...) {
        JS_ThrowInternalError(ctx, "Unknown error");
        return JS_EXCEPTION;
    }
}

/** Same as wrap_call, but pass this_value as first argument.
 * @tparam FirstArg type of this_value
 */
template <typename R, typename FirstArg, typename... Args, typename Callable>
JSValue wrap_this_call(JSContext *ctx, Callable &&f, JSValueConst this_value,
                       int argc, JSValueConst *argv) noexcept {
    try {
        setCurrentContext(ctx);
        if constexpr (std::is_same_v<R, void>) {
            std::apply(
                std::forward<Callable>(f),
                std::tuple_cat(unwrap_args<FirstArg>(ctx, 1, &this_value),
                               unwrap_args<Args...>(ctx, argc, argv)));
            return JS_NULL;
        } else {
            return js_traits<std::decay_t<R>>::wrap(
                ctx, std::apply(std::forward<Callable>(f),
                                std::tuple_cat(
                                    unwrap_args<FirstArg>(ctx, 1, &this_value),
                                    unwrap_args<Args...>(ctx, argc, argv))));
        }
    } catch (exception) {
        return JS_EXCEPTION;
    } catch (std::exception const &err) {
        JS_ThrowInternalError(ctx, "%s", err.what());
        return JS_EXCEPTION;
    } catch (...) {
        JS_ThrowInternalError(ctx, "Unknown error");
        return JS_EXCEPTION;
    }
}

template <class Tuple, std::size_t... I>
void wrap_args_impl(JSContext *ctx, JSValue *argv, Tuple tuple,
                    std::index_sequence<I...>) {
    ((argv[I] = js_traits<std::decay_t<std::tuple_element_t<I, Tuple>>>::wrap(
          ctx, std::get<I>(tuple))),
     ...);
}

/** Converts C++ args to JSValue array.
 * @tparam Args argument types
 * @param argv array of size at least sizeof...(Args)
 */
template <typename... Args>
void wrap_args(JSContext *ctx, JSValue *argv, Args &&...args) {
    wrap_args_impl(ctx, argv, std::make_tuple(std::forward<Args>(args)...),
                   std::make_index_sequence<sizeof...(Args)>());
}

// Helper trait to obtain `T` in `T::*` expressions
template <typename T> struct class_from_member_pointer {
    using type = void;
};
template <typename T, typename U> struct class_from_member_pointer<T U::*> {
    using type = U;
};
template <typename T>
using class_from_member_pointer_t = typename class_from_member_pointer<T>::type;

} // namespace detail

/** A wrapper type for free and class member functions.
 * Pointer to function F is a template argument.
 * @tparam F either a pointer to free function or a pointer to class member
 * function
 * @tparam PassThis if true and F is a pointer to free function, passes
 * Javascript "this" value as first argument:
 */
template <auto F, bool PassThis = false /* pass this as the first argument */>
struct fwrapper {
    /// "name" property of the JS function object (not defined if nullptr)
    const char *name = nullptr;
};

/** Conversion to JSValue for free function in fwrapper. */
template <typename R, typename... Args, R (*F)(Args...), bool PassThis>
struct js_traits<fwrapper<F, PassThis>> {
    static JSValue wrap(JSContext *ctx, fwrapper<F, PassThis> fw) noexcept {
        return JS_NewCFunction(
            ctx,
            [](JSContext *ctx, JSValueConst this_value, int argc,
               JSValueConst *argv) noexcept -> JSValue {
                if constexpr (PassThis)
                    return detail::wrap_this_call<R, Args...>(
                        ctx, F, this_value, argc, argv);
                else
                    return detail::wrap_call<R, Args...>(ctx, F, argc, argv);
            },
            fw.name, sizeof...(Args));
    }
};

/** Conversion to JSValue for class member function in fwrapper. PassThis is
 * ignored and treated as true */
template <typename R, class T, typename... Args, R (T::*F)(Args...),
          bool PassThis /*=ignored*/>
struct js_traits<fwrapper<F, PassThis>> {
    static JSValue wrap(JSContext *ctx, fwrapper<F, PassThis> fw) noexcept {
        return JS_NewCFunction(
            ctx,
            [](JSContext *ctx, JSValueConst this_value, int argc,
               JSValueConst *argv) noexcept -> JSValue {
                return detail::wrap_this_call<R, std::shared_ptr<T>, Args...>(
                    ctx, F, this_value, argc, argv);
            },
            fw.name, sizeof...(Args));
    }
};

/** Conversion to JSValue for const class member function in fwrapper. PassThis
 * is ignored and treated as true */
template <typename R, class T, typename... Args, R (T::*F)(Args...) const,
          bool PassThis /*=ignored*/>
struct js_traits<fwrapper<F, PassThis>> {
    static JSValue wrap(JSContext *ctx, fwrapper<F, PassThis> fw) noexcept {
        return JS_NewCFunction(
            ctx,
            [](JSContext *ctx, JSValueConst this_value, int argc,
               JSValueConst *argv) noexcept -> JSValue {
                return detail::wrap_this_call<R, std::shared_ptr<T>, Args...>(
                    ctx, F, this_value, argc, argv);
            },
            fw.name, sizeof...(Args));
    }
};

/** A wrapper type for constructor of type T with arguments Args.
 * Compilation fails if no such constructor is defined.
 * @tparam Args constructor arguments
 */
template <class T, typename... Args> struct ctor_wrapper {
    static_assert(std::is_constructible<T, Args...>::value,
                  "no such constructor!");
    /// "name" property of JS constructor object
    const char *name = nullptr;
};

namespace detail {
/// equivalent to JS_GetPropertyStr(ctx, this_value, "prototype");
inline JSValue GetPropertyPrototype(JSContext *ctx, JSValueConst this_value) {
    // constant atom: doesn't need to be freed and doesn't change with context
    static const JSAtom JS_ATOM_prototype = JS_NewAtom(ctx, "prototype");
    return JS_GetProperty(ctx, this_value, JS_ATOM_prototype);
}
} // namespace detail

/** Conversion to JSValue for ctor_wrapper. */
template <class T, typename... Args>
struct js_traits<ctor_wrapper<T, Args...>> {
    static JSValue wrap(JSContext *ctx, ctor_wrapper<T, Args...> cw) noexcept {
        return JS_NewCFunction2(
            ctx,
            [](JSContext *ctx, JSValueConst this_value, int argc,
               JSValueConst *argv) noexcept -> JSValue {
                if (js_traits<std::shared_ptr<T>>::QJSClassId ==
                    0) // not registered
                {
#if defined(__cpp_rtti)
                    // automatically register class on first use (no prototype)
                    js_traits<std::shared_ptr<T>>::register_class(
                        ctx, typeid(T).name());
#else
                    JS_ThrowTypeError(ctx, "quickjspp ctor_wrapper<T>::wrap: "
                                           "Class is not registered");
                    return JS_EXCEPTION;
#endif
                }

                auto proto = detail::GetPropertyPrototype(ctx, this_value);
                if (JS_IsException(proto))
                    return proto;
                auto jsobj = JS_NewObjectProtoClass(
                    ctx, proto, js_traits<std::shared_ptr<T>>::QJSClassId);
                JS_FreeValue(ctx, proto);
                if (JS_IsException(jsobj))
                    return jsobj;

                try {
                    std::shared_ptr<T> ptr = std::apply(
                        std::make_shared<T, Args...>,
                        detail::unwrap_args<Args...>(ctx, argc, argv));
                    JS_SetOpaque(jsobj, new std::shared_ptr<T>(std::move(ptr)));
                    return jsobj;
                } catch (exception) {
                    JS_FreeValue(ctx, jsobj);
                    return JS_EXCEPTION;
                } catch (std::exception const &err) {
                    JS_FreeValue(ctx, jsobj);
                    JS_ThrowInternalError(ctx, "%s", err.what());
                    return JS_EXCEPTION;
                } catch (...) {
                    JS_FreeValue(ctx, jsobj);
                    JS_ThrowInternalError(ctx, "Unknown error");
                    return JS_EXCEPTION;
                }

                // return detail::wrap_call<std::shared_ptr<T>, Args...>(ctx,
                // std::make_shared<T, Args...>, argv);
            },
            cw.name, sizeof...(Args), JS_CFUNC_constructor, 0);
    }
};

/** Conversions for std::shared_ptr<T>. Empty shared_ptr corresponds to JS_NULL.
 * T should be registered to a context before conversions.
 * @tparam T class type
 */
template <class T> struct js_traits<std::shared_ptr<T>> {
    /// Registered class id in QuickJS.
    inline static JSClassID QJSClassId = 0;

    /// Signature of the function to obtain the std::shared_ptr from the
    /// JSValue.
    using ptr_cast_fcn_t =
        std::function<std::shared_ptr<T>(JSContext *, JSValueConst)>;

    /// Used by registerDerivedClass to register new derived classes with this
    /// class' base type.
    inline static std::function<void(JSClassID, ptr_cast_fcn_t)>
        registerWithBase;

    /// Mapping between derived class' JSClassID and function to obtain the
    /// std::shared_ptr from the JSValue.
    inline static std::unordered_map<JSClassID, ptr_cast_fcn_t> ptrCastFcnMap;

    /** Register a class as a derived class.
     *
     * @tparam D type of the derived class
     * @param derived_class_id class id of the derived class
     * @param ptr_cast_fcn function to obtain a std::shared_ptr from the JSValue
     */
    template <typename D>
    static void registerDerivedClass(JSClassID derived_class_id,
                                     ptr_cast_fcn_t ptr_cast_fcn) {
        static_assert(std::is_base_of<T, D>::value &&
                          !std::is_same<T, D>::value,
                      "Type is not a derived class");
        using derived_ptr_cast_fcn_t =
            typename js_traits<std::shared_ptr<D>>::ptr_cast_fcn_t;

        // Register how to obtain the std::shared_ptr from the derived class.
        ptrCastFcnMap[derived_class_id] = ptr_cast_fcn;

        // Propagate the registration to our base class (if any).
        if (registerWithBase)
            registerWithBase(derived_class_id, ptr_cast_fcn);

        // Instrument the derived class so that it can propagate new derived
        // classes to us.
        auto old_registerWithBase =
            js_traits<std::shared_ptr<D>>::registerWithBase;
        js_traits<std::shared_ptr<D>>::registerWithBase =
            [old_registerWithBase = std::move(old_registerWithBase)](
                JSClassID derived_class_id,
                derived_ptr_cast_fcn_t derived_ptr_cast_fcn) {
                if (old_registerWithBase)
                    old_registerWithBase(derived_class_id,
                                         derived_ptr_cast_fcn);
                registerDerivedClass<D>(
                    derived_class_id,
                    [derived_cast_fcn = std::move(derived_ptr_cast_fcn)](
                        JSContext *ctx, JSValueConst v) {
                        return std::shared_ptr<T>(derived_cast_fcn(ctx, v));
                    });
            };
    }

    template <typename B>
    static std::enable_if_t<std::is_same_v<B, T> || std::is_same_v<B, void>>
    ensureCanCastToBase(JSContext *ctx) {}

    template <typename B>
    static std::enable_if_t<!std::is_same_v<B, T> && !std::is_same_v<B, void>>
    ensureCanCastToBase(JSContext *ctx) {
        static_assert(std::is_base_of_v<B, T>, "Type is not a derived class");

        if (js_traits<std::shared_ptr<T>>::QJSClassId == 0)
            JS_NewClassID(JS_GetRuntime(ctx),
                          &js_traits<std::shared_ptr<T>>::QJSClassId);

        js_traits<std::shared_ptr<B>>::template registerDerivedClass<T>(
            QJSClassId, unwrap);
    }

    template <auto M> static void ensureCanCastToBase(JSContext *ctx) {
        ensureCanCastToBase<detail::class_from_member_pointer_t<decltype(M)>>(
            ctx);
    }

    /** Stores offsets to qjs::Value members of T.
     * These values should be marked by class_registrar::mark for QuickJS
     * garbage collector so that the cycle removal algorithm can find the other
     * objects referenced by this object.
     */
    static inline std::vector<Value T::*> markOffsets;

    /** Register class in QuickJS context.
     *
     * @param ctx context
     * @param name class name
     * @param proto class prototype or JS_NULL
     * @param call QJS call function. see quickjs doc
     * @param exotic pointer to QJS exotic methods(static lifetime) which allow
     * custom property handling. see quickjs doc
     * @throws exception
     */
    static void register_class(JSContext *ctx, const char *name,
                               JSValue proto = JS_NULL,
                               JSClassCall *call = nullptr,
                               JSClassExoticMethods *exotic = nullptr) {
        if (QJSClassId == 0) {
            JS_NewClassID(JS_GetRuntime(ctx), &QJSClassId);
        }
        auto rt = JS_GetRuntime(ctx);
        if (!JS_IsRegisteredClass(rt, QJSClassId)) {
            JSClassGCMark *marker = nullptr;
            if (!markOffsets.empty()) {
                marker = [](JSRuntime *rt, JSValueConst val,
                            JS_MarkFunc *mark_func) {
                    auto pptr = static_cast<std::shared_ptr<T> *>(
                        JS_GetOpaque(val, QJSClassId));
                    assert(pptr);
                    const T *ptr = pptr->get();
                    assert(ptr);
                    for (Value T::*member : markOffsets) {
                        JS_MarkValue(rt, (*ptr.*member).v, mark_func);
                    }
                };
            }
            JSClassDef def{name,
                           // destructor (finalizer)
                           [](JSRuntime *rt, JSValue obj) noexcept {
                               auto pptr = static_cast<std::shared_ptr<T> *>(
                                   JS_GetOpaque(obj, QJSClassId));
                               delete pptr;
                           },
                           // mark
                           marker,
                           // call
                           call,
                           // exotic
                           exotic};
            int e = JS_NewClass(rt, QJSClassId, &def);
            if (e < 0) {
                JS_ThrowInternalError(ctx, "Can't register class %s", name);
                throw exception{ctx};
            }
        }
        JS_SetClassProto(ctx, QJSClassId, proto);
    }

    /** Create a JSValue from std::shared_ptr<T>.
     * Creates an object with class if #QJSClassId and sets its opaque pointer
     * to a new copy of #ptr.
     */
    static JSValue wrap(JSContext *ctx, std::shared_ptr<T> ptr) {
        if (!ptr)
            return JS_NULL;
        if (QJSClassId == 0) // not registered
        {
#if defined(__cpp_rtti)
            // automatically register class on first use (no prototype)
            register_class(ctx, typeid(T).name());
#else
            JS_ThrowTypeError(
                ctx,
                "quickjspp std::shared_ptr<T>::wrap: Class is not registered");
            return JS_EXCEPTION;
#endif
        }
        auto jsobj = JS_NewObjectClass(ctx, QJSClassId);
        if (JS_IsException(jsobj))
            return jsobj;

        auto pptr = new std::shared_ptr<T>(std::move(ptr));
        JS_SetOpaque(jsobj, pptr);
        return jsobj;
    }

    /// @throws exception if #v doesn't have the correct class id
    static std::shared_ptr<T> unwrap(JSContext *ctx, JSValueConst v) {
        std::shared_ptr<T> ptr = nullptr;
        if (JS_IsNull(v)) {
            return ptr;
        }
        auto obj_class_id = JS_GetClassID(v);

        if (obj_class_id == QJSClassId) {
            // The JS object is of class T
            void *opaque = JS_GetOpaque2(ctx, v, obj_class_id);
            assert(opaque && "No opaque pointer in object");
            ptr = *static_cast<std::shared_ptr<T> *>(opaque);
        } else if (ptrCastFcnMap.count(obj_class_id)) {
            // The JS object is of a class derived from T
            ptr = ptrCastFcnMap[obj_class_id](ctx, v);
        } else {
            // The JS object does not derives from T
            JS_ThrowTypeError(ctx,
                              "Expected type %s, got object with classid %d",
                              QJSPP_TYPENAME(T), obj_class_id);
            throw exception{ctx};
        }
        if (!ptr) {
            JS_ThrowInternalError(ctx, "Object's opaque pointer is NULL");
            throw exception{ctx};
        }
        return ptr;
    }
};

/** Conversions for non-owning pointers to class T. nullptr corresponds to
 * JS_NULL.
 * @tparam T class type
 */
template <class T> struct js_traits<T *, std::enable_if_t<std::is_class_v<T>>> {
    static JSValue wrap(JSContext *ctx, T *ptr) {
        if (ptr == nullptr) {
            return JS_NULL;
        }
        if (js_traits<std::shared_ptr<T>>::QJSClassId == 0) // not registered
        {
#if defined(__cpp_rtti)
            // If you have an error here with T=JSValueConst
            // it probably means you are passing JSValueConst to where JSValue
            // is expected
            js_traits<std::shared_ptr<T>>::register_class(ctx,
                                                          typeid(T).name());
#else
            JS_ThrowTypeError(
                ctx, "quickjspp js_traits<T *>::wrap: Class is not registered");
            return JS_EXCEPTION;
#endif
        }
        auto jsobj =
            JS_NewObjectClass(ctx, js_traits<std::shared_ptr<T>>::QJSClassId);
        if (JS_IsException(jsobj))
            return jsobj;

        // shared_ptr with empty deleter since we don't own T*
        auto pptr = new std::shared_ptr<T>(ptr, [](T *) {});
        JS_SetOpaque(jsobj, pptr);
        return jsobj;
    }

    static T *unwrap(JSContext *ctx, JSValueConst v) {
        if (JS_IsNull(v)) {
            return nullptr;
        }
        auto ptr = js_traits<std::shared_ptr<T>>::unwrap(ctx, v);
        return ptr.get();
    }
};

/** Conversions for enums. */
template <typename E> struct js_traits<E, std::enable_if_t<std::is_enum_v<E>>> {
    using T = std::underlying_type_t<E>;
    static E unwrap(JSContext *ctx, JSValue v) noexcept {
        return static_cast<E>(js_traits<T>::unwrap(ctx, v));
    }

    static JSValue wrap(JSContext *ctx, E t) noexcept {
        return js_traits<T>::wrap(ctx, static_cast<T>(t));
        ;
    }
};

namespace detail {
/** A faster std::function-like object with type erasure.
 * Used to convert any callable objects (including lambdas) to JSValue.
 */
struct function {
    JSValue (*invoker)(function *self, JSContext *ctx, JSValueConst this_value,
                       int argc, JSValueConst *argv) = nullptr;

    void (*destroyer)(function *self) = nullptr;

    alignas(std::max_align_t) char functor[];

    template <typename Functor>
    static function *create(JSRuntime *rt, Functor &&f) {
        using Functor_t = std::decay_t<Functor>;
        auto fptr = static_cast<function *>(
            js_malloc_rt(rt, sizeof(function) + sizeof(Functor_t)));
        if (!fptr)
            throw std::bad_alloc{};
        new (fptr) function;
        auto functorptr = reinterpret_cast<Functor_t *>(fptr->functor);
        new (functorptr) Functor_t(std::forward<Functor>(f));
        fptr->destroyer = nullptr;
        if constexpr (!std::is_trivially_destructible_v<Functor_t>) {
            fptr->destroyer = [](function *fptr) {
                auto functorptr = reinterpret_cast<Functor_t *>(fptr->functor);
                functorptr->~Functor_t();
            };
        }
        return fptr;
    }
};

static_assert(std::is_trivially_destructible_v<function>);
} // namespace detail

template <> struct js_traits<detail::function> {
    inline static JSClassID QJSClassId = 0;

    // TODO: replace ctx with rt
    static void register_class(JSContext *ctx, const char *name) {
        if (QJSClassId == 0) {

            JS_NewClassID(JS_GetRuntime(ctx), &QJSClassId);
        }
        auto rt = JS_GetRuntime(ctx);
        if (JS_IsRegisteredClass(rt, QJSClassId))
            return;
        JSClassDef def{name,
                       // destructor
                       [](JSRuntime *rt, JSValue obj) noexcept {
                           auto fptr = static_cast<detail::function *>(
                               JS_GetOpaque(obj, QJSClassId));
                           assert(fptr);
                           if (fptr->destroyer)
                               fptr->destroyer(fptr);
                           js_free_rt(rt, fptr);
                       },
                       nullptr, // mark
                       // call
                       [](JSContext *ctx, JSValueConst func_obj,
                          JSValueConst this_val, int argc, JSValueConst *argv,
                          int flags) -> JSValue {
                           auto ptr = static_cast<detail::function *>(
                               JS_GetOpaque2(ctx, func_obj, QJSClassId));
                           if (!ptr)
                               return JS_EXCEPTION;
                           return ptr->invoker(ptr, ctx, this_val, argc, argv);
                       },
                       nullptr};
        int e = JS_NewClass(rt, QJSClassId, &def);
        if (e < 0)
            throw std::runtime_error{"Cannot register C++ function class"};
    }
};

/** Traits for accessing object properties.
 * @tparam Key property key type (uint32 and strings are supported)
 */
template <typename Key> struct js_property_traits {
    static void set_property(JSContext *ctx, JSValue this_obj, Key key,
                             JSValue value);

    static JSValue get_property(JSContext *ctx, JSValue this_obj, Key key);
};

template <> struct js_property_traits<const char *> {
    static void set_property(JSContext *ctx, JSValue this_obj, const char *name,
                             JSValue value) {
        int err = JS_SetPropertyStr(ctx, this_obj, name, value);
        if (err < 0)
            throw exception{ctx};
    }

    static JSValue get_property(JSContext *ctx, JSValue this_obj,
                                const char *name) noexcept {
        return JS_GetPropertyStr(ctx, this_obj, name);
    }
};

template <> struct js_property_traits<std::string> {
    static void set_property(JSContext *ctx, JSValue this_obj,
                             const std::string &name, JSValue value) {
        int err = JS_SetPropertyStr(ctx, this_obj, name.c_str(), value);
        if (err < 0)
            throw exception{ctx};
    }

    static JSValue get_property(JSContext *ctx, JSValue this_obj,
                                const std::string &name) noexcept {
        return JS_GetPropertyStr(ctx, this_obj, name.c_str());
    }
};

template <> struct js_property_traits<std::string_view> {
    static void set_property(JSContext *ctx, JSValue this_obj,
                             std::string_view name, JSValue value) {
        int err = JS_SetPropertyStr(ctx, this_obj, name.data(), value);
        if (err < 0)
            throw exception{ctx};
    }

    static JSValue get_property(JSContext *ctx, JSValue this_obj,
                                std::string_view name) noexcept {
        return JS_GetPropertyStr(ctx, this_obj, name.data());
    }
};

template <> struct js_property_traits<uint32_t> {
    static void set_property(JSContext *ctx, JSValue this_obj, uint32_t idx,
                             JSValue value) {
        int err = JS_SetPropertyUint32(ctx, this_obj, idx, value);
        if (err < 0)
            throw exception{ctx};
    }

    static JSValue get_property(JSContext *ctx, JSValue this_obj,
                                uint32_t idx) noexcept {
        return JS_GetPropertyUint32(ctx, this_obj, idx);
    }
};

template <> struct js_property_traits<int> : js_property_traits<uint32_t> {};

namespace detail {
template <typename Key> struct property_proxy {
    JSContext *ctx;
    JSValue this_obj;
    Key key;

    /** Conversion helper function */
    template <typename T> T as() const {
        return unwrap_free<T>(
            ctx, js_property_traits<Key>::get_property(ctx, this_obj, key));
    }

    /** Explicit conversion operator (to any type) */
    template <typename T> explicit operator T() const { return as<T>(); }

    /** Implicit converion to qjs::Value */
    operator Value() const; // defined later due to Value being incomplete type

    /// noncopyable
    property_proxy &operator=(property_proxy) = delete;

    template <typename T> property_proxy &operator=(T &&value) {
        js_property_traits<Key>::set_property(
            ctx, this_obj, key,
            js_traits<std::decay_t<T>>::wrap(ctx, std::forward<T>(value)));
        return *this;
    }

    template <typename Key2> property_proxy<Key2> operator[](Key2 key2) const {
        return {ctx, as<JSValue>(), std::move(key2)};
    }

    ~property_proxy() noexcept { JS_FreeValue(ctx, this_obj); }
};

// class member variable getter/setter
template <auto M> struct get_set {};

// M -  member object
template <class T, typename R, R T::*M> struct get_set<M> {
    using is_const = std::is_const<R>;

    static const R &get(std::shared_ptr<T> ptr) { return *ptr.*M; }

    static R &set(std::shared_ptr<T> ptr, R value) {
        return *ptr.*M = std::move(value);
    }
};

// M - static member object
template <typename R, R *M> struct get_set<M> {
    using is_const = std::is_const<R>;

    static const R &get(bool) { return *M; }

    static R &set(bool, R value) { return *M = std::move(value); }
};

} // namespace detail

/** JSValue with RAAI semantics.
 * A wrapper over (JSValue v, JSContext * ctx).
 * Calls JS_FreeValue(ctx, v) on destruction. Can be copied and moved.
 * A JSValue can be released by either JSValue x = std::move(value); or JSValue
 * x = value.release(), then the Value becomes invalid and FreeValue won't be
 * called Can be converted to C++ type, for example: auto string =
 * value.as<std::string>(); qjs::exception would be thrown on error Properties
 * can be accessed (read/write): value["property1"] = 1; value[2] = "2";
 */
class Value {
public:
    JSValue v;
    JSContext *ctx = nullptr;
    // if ctx_holder is expired, do not free v
    std::optional<std::weak_ptr<Context>> ctx_holder;

public:
    /** Use context.newValue(val) instead */
    template <typename T> Value(JSContext *ctx, T &&val) : ctx(ctx) {
        v = js_traits<std::decay_t<T>>::wrap(ctx, std::forward<T>(val));
        if (JS_IsException(v))
            throw exception{ctx};
    }

    Value(std::weak_ptr<Context> ctx, auto &&val) {
        ctx_holder = ctx;
        this->ctx = getContextFromWrapped(ctx.lock().get());
        v = js_traits<std::decay_t<decltype(val)>>::wrap(
            this->ctx, std::forward<decltype(val)>(val));
        if (JS_IsException(v))
            throw exception{this->ctx};
    }

    Value(JSValue &&v) noexcept : v(std::move(v)), ctx(nullptr) {}

    Value(const Value &rhs) noexcept {
        ctx = rhs.ctx;
        v = JS_DupValue(ctx, rhs.v);
    }

    Value(Value &&rhs) noexcept {
        std::swap(ctx, rhs.ctx);
        v = rhs.v;
    }

    Value &operator=(Value rhs) noexcept {
        std::swap(ctx, rhs.ctx);
        std::swap(v, rhs.v);
        return *this;
    }

    bool operator==(JSValueConst other) const {
        return JS_VALUE_GET_TAG(v) == JS_VALUE_GET_TAG(other) &&
               JS_VALUE_GET_PTR(v) == JS_VALUE_GET_PTR(other);
    }

    bool operator!=(JSValueConst other) const { return !((*this) == other); }

    /** Returns true if 2 values are the same (equality for arithmetic types or
     * point to the same object) */
    bool operator==(const Value &rhs) const { return (*this == rhs.v); }

    bool operator!=(const Value &rhs) const { return !((*this) == rhs); }

    ~Value() {
        if (ctx && (ctx_holder.has_value() && !ctx_holder.value().expired()))
            JS_FreeValue(ctx, v);
    }

    bool isError() const { return JS_IsError(v); }

    /** Conversion helper function: value.as<T>()
     * @tparam T type to convert to
     * @return type returned by js_traits<std::decay_t<T>>::unwrap that should
     * be implicitly convertible to T
     * */
    template <typename T> auto as() const {
        return js_traits<std::decay_t<T>>::unwrap(ctx, v);
    }

    /** Explicit conversion: static_cast<T>(value) or (T)value */
    template <typename T> explicit operator T() const { return as<T>(); }

    JSValue release() noexcept // dont call freevalue
    {
        ctx = nullptr;
        return v;
    }

    /** Implicit conversion to JSValue (rvalue only). Example: JSValue v =
     * std::move(value); */
    operator JSValue() && noexcept { return release(); }

    /** Access JS properties. Returns proxy type which is implicitly convertible
     * to qjs::Value */
    template <typename Key> detail::property_proxy<Key> operator[](Key key) {
        assert(ctx && "Trying to access properties of Value with no JSContext");
        return {ctx, JS_DupValue(ctx, v), std::move(key)};
    }

    // add("f", []() {...});
    template <typename Function> Value &add(const char *name, Function &&f) {
        (*this)[name] =
            js_traits<decltype(std::function{std::forward<Function>(f)})>::wrap(
                ctx, std::forward<Function>(f));
        return *this;
    }

    // add<&f>("f");
    // add<&T::f>("f");
    template <auto F>
    std::enable_if_t<std::is_member_function_pointer_v<decltype(F)> ||
                         std::is_function_v<std::remove_pointer_t<decltype(F)>>,
                     Value &>
    add(const char *name) {
        (*this)[name] = fwrapper<F>{name};
        return *this;
    }

    // add_getter_setter<&T::get_member, &T::set_member>("member");
    template <auto FGet, auto FSet> Value &add_getter_setter(const char *name) {
        auto prop = JS_NewAtom(ctx, name);
        using fgetter = fwrapper<FGet, true>;
        using fsetter = fwrapper<FSet, true>;
        int ret = JS_DefinePropertyGetSet(
            ctx, v, prop, js_traits<fgetter>::wrap(ctx, fgetter{name}),
            js_traits<fsetter>::wrap(ctx, fsetter{name}),
            JS_PROP_CONFIGURABLE | JS_PROP_WRITABLE | JS_PROP_ENUMERABLE);
        JS_FreeAtom(ctx, prop);
        if (ret < 0)
            throw exception{ctx};
        return *this;
    }

    // add_getter<&T::get_member>("member");
    template <auto FGet> Value &add_getter(const char *name) {
        auto prop = JS_NewAtom(ctx, name);
        using fgetter = fwrapper<FGet, true>;
        int ret = JS_DefinePropertyGetSet(
            ctx, v, prop, js_traits<fgetter>::wrap(ctx, fgetter{name}),
            JS_UNDEFINED, JS_PROP_CONFIGURABLE | JS_PROP_ENUMERABLE);
        JS_FreeAtom(ctx, prop);
        if (ret < 0)
            throw exception{ctx};
        return *this;
    }

    // add<&T::member>("member");
    template <auto M>
    std::enable_if_t<std::is_member_object_pointer_v<decltype(M)>, Value &>
    add(const char *name) {
        if constexpr (detail::get_set<M>::is_const::value)
            return add_getter<detail::get_set<M>::get>(name);
        else
            return add_getter_setter<detail::get_set<M>::get,
                                     detail::get_set<M>::set>(name);
    }

    // add<&T::static_member>("static_member");
    template <auto M>
    std::enable_if_t<
        std::is_pointer_v<decltype(M)> &&
            !std::is_function_v<std::remove_pointer_t<decltype(M)>>,
        Value &>
    add(const char *name) {
        if constexpr (detail::get_set<M>::is_const::value)
            return add_getter<detail::get_set<M>::get>(name);
        else
            return add_getter_setter<detail::get_set<M>::get,
                                     detail::get_set<M>::set>(name);
    }

    std::string toJSON(const Value &replacer = JS_UNDEFINED,
                       const Value &space = JS_UNDEFINED) {
        assert(ctx);
        assert(!replacer.ctx || ctx == replacer.ctx);
        assert(!space.ctx || ctx == space.ctx);
        return (std::string)Value{
            weakFromContext(ctx),
            JS_JSONStringify(ctx, v, replacer.v, space.v)};
    }

    /** same as Context::eval() but with this Value as 'this' */
    Value evalThis(std::string_view buffer, const char *filename = "<evalThis>",
                   int flags = 0) {
        assert(buffer.data()[buffer.size()] == '\0' &&
               "eval buffer is not null-terminated"); // JS_Eval requirement
        assert(ctx);
        return Value{
            weakFromContext(ctx),
            JS_EvalThis(ctx, v, buffer.data(), buffer.size(), filename, flags)};
    }
};

/** Thin wrapper over JSRuntime * rt
 * Calls JS_FreeRuntime on destruction. noncopyable.
 */
class Runtime {
public:
    JSRuntime *rt;

    Runtime() {
        rt = JS_NewRuntime();
        JS_SetMaxStackSize(rt, 0);
        if (!rt)
            throw std::runtime_error{"qjs: Cannot create runtime"};

        JS_SetHostPromiseRejectionTracker(
            rt, promise_unhandled_rejection_tracker, NULL);
        JS_SetModuleLoaderFunc(rt, nullptr, module_loader, nullptr);
    }

    // noncopyable
    Runtime(const Runtime &) = delete;

    ~Runtime() { JS_FreeRuntime(rt); }

    /// @return pointer to qjs::Context of the executed job or nullptr if no job
    /// is pending
    Context *executePendingJob();

    bool isJobPending() const { return JS_IsJobPending(rt); }

private:
    static void promise_unhandled_rejection_tracker(JSContext *ctx,
                                                    JSValue promise,
                                                    JSValue reason,
                                                    bool is_handled,
                                                    void *opaque);

    static JSModuleDef *module_loader(JSContext *ctx, const char *module_name,
                                      void *opaque);
};

namespace detail {

inline std::optional<std::string>
readFile(std::filesystem::path const &filepath) {
    if (!std::filesystem::exists(filepath))
        return std::nullopt;
    std::ifstream f(filepath, std::ios::in | std::ios::binary);
    if (!f.is_open())
        return std::nullopt;
    std::stringstream sstream;
    sstream << f.rdbuf();
    return sstream.str();
}

inline std::string toUri(std::string_view filename) {
    auto fname = std::string{filename};
    if (fname.find("://") < fname.find("/"))
        return fname;

    auto fpath = std::filesystem::path(fname);
    if (!fpath.is_absolute()) {
        fpath = "." / fpath;
    }
    fpath = std::filesystem::weakly_canonical(fpath);
    fname = "file://" + fpath.generic_string();
    return fname;
}

} // namespace detail

/** Wrapper over JSContext * ctx
 * Calls JS_SetContextOpaque(ctx, this); on construction and JS_FreeContext on
 * destruction
 */
class Context : public std::enable_shared_from_this<Context> {
public:
    JSContext *ctx;
    thread_local static Context *current;
    // for starting jobs quickly
    std::condition_variable js_job_start_cv = {};
    std::mutex js_job_start_mutex = {};
    bool has_pending_job = false;
    /** Module wrapper
     * Workaround for lack of opaque pointer for module load function by keeping
     * a list of modules in qjs::Context.
     */
    class Module {
        friend class Context;

        JSModuleDef *m;
        JSContext *ctx;
        const char *name;

        using nvp = std::pair<std::string, Value>;
        std::vector<nvp> exports;

    public:
        Module(JSContext *ctx, const char *name) : ctx(ctx), name(name) {
            m = JS_NewCModule(
                ctx, name, [](JSContext *ctx, JSModuleDef *m) noexcept {
                    auto &context = Context::get(ctx);
                    auto it = std::find_if(
                        context.modules.begin(), context.modules.end(),
                        [m](const Module &module) { return module.m == m; });
                    if (it == context.modules.end())
                        return -1;

                    std::unordered_map<std::string, Value> namespaces;
                    auto export_obj = Value{ctx, JS_NewObject(ctx)};

                    auto set_key_recursive =
                        [ctx](this auto &self, Value obj,
                              const std::vector<std::string_view> &keys,
                              Value &value) -> void {
                        if (keys.size() == 1) {
                            JS_SetPropertyStr(ctx, obj.v, keys[0].data(),
                                              value.v);
                        } else {
                            auto key = keys[0];
                            auto keys_rest = std::vector<std::string_view>(
                                keys.begin() + 1, keys.end());
                            Value sub_obj =
                                JS_GetPropertyStr(ctx, obj.v, key.data());
                            if (JS_IsUndefined(sub_obj.v)) {
                                sub_obj = JS_NewObject(ctx);
                                JS_SetPropertyStr(ctx, obj.v, key.data(),
                                                  sub_obj.v);
                            }
                            self(sub_obj, keys_rest, value);
                        }
                    };

                    // resort the exports to ensure that the shorter namespaces
                    // are processed before the longer ones, so that we can
                    // create nested objects correctly
                    std::ranges::sort(
                        it->exports, [](const nvp &a, const nvp &b) {
                            return a.first.size() < b.first.size();
                        });

                    for (auto &e : it->exports) {
                        auto string_path = std::string(e.first);
                        auto path = string_path |
                                    std::views::split(std::string_view("::")) |
                                    std::ranges::to<std::vector<std::string>>();
                        auto view_path = std::vector<std::string_view>(
                            path.begin(), path.end());

                        set_key_recursive(export_obj, view_path, e.second);
                        JS_SetModuleExport(ctx, m, path[0].data(),
                                           JS_GetPropertyStr(ctx, export_obj.v,
                                                             path[0].data()));
                    }

                    return 0;
                });
            if (!m)
                throw exception{ctx};
        }

        Module &add(std::string_view name, JSValue &&value) {
            exports.push_back({std::string(name), {ctx, std::move(value)}});
            JS_AddModuleExport(
                ctx, m, std::string(name.substr(0, name.find("::"))).data());
            return *this;
        }

        template <typename T> Module &add(std::string_view name, T &&value) {
            return add(name, js_traits<T>::wrap(ctx, std::forward<T>(value)));
        }

        Module(const Module &) = delete;

        Module(Module &&) = default;
        // Module& operator=(Module&&) = default;

        // function wrappers

        /** Add free function F.
         * Example:
         * module.function<static_cast<double (*)(double)>(&::sin)>("sin");
         */
        template <auto F> Module &function(const char *name) {
            return add(name, qjs::fwrapper<F>{name});
        }

        /** Add function object f.
         * Slower than template version.
         * Example: module.function("sin", [](double x) { return ::sin(x); });
         */
        template <typename F> Module &function(const char *name, F &&f) {
            return add(
                name,
                js_traits<decltype(std::function{std::forward<F>(f)})>::wrap(
                    ctx, std::forward<F>(f)));
        }

        // class register wrapper
    private:
        /** Helper class to register class members and constructors.
         * See fun, constructor.
         * Actual registration occurs at object destruction.
         */
        template <class T> class class_registrar {
            const char *name;
            qjs::Value prototype;
            qjs::Context::Module &module;
            qjs::Context &context;
            qjs::Value ctor; // last added constructor
        public:
            explicit class_registrar(const char *name,
                                     qjs::Context::Module &module,
                                     qjs::Context &context)
                : name(name), prototype(context.newObject()), module(module),
                  context(context), ctor(JS_NULL) {}

            class_registrar(const class_registrar &) = delete;

            /** Add functional object f
             */
            template <typename F>
            class_registrar &fun(const char *name, F &&f) {
                prototype[name] = std::forward<F>(f);
                return *this;
            }

            /** Add class member function or class member variable F
             * Example:
             * struct T { int var; int func(); }
             * auto& module = context.addModule("module");
             * module.class_<T>("T").fun<&T::var>("var").fun<&T::func>("func");
             */
            template <auto F> class_registrar &fun(const char *name) {
                js_traits<std::shared_ptr<T>>::template ensureCanCastToBase<F>(
                    context.ctx);
                prototype.add<F>(name);
                return *this;
            }

            /** Add a static member or function to the last added constructor.
             * Example:
             *  struct T { static int var; static int func(); }
             *  module.class_<T>("T").contructor<>("T").static_fun<&T::var>("var").static_fun<&T::func>("func");
             */
            template <auto F> class_registrar &static_fun(const char *name) {
                assert(!JS_IsNull(ctor.v) &&
                       "You should call .constructor before .static_fun");
                js_traits<qjs::shared_ptr<T>>::template ensureCanCastToBase<F>(
                    context.ctx);
                ctor.add<F>(name);
                return *this;
            }

            /** Add a property with custom getter and setter.
             * Example:
             * module.class_<T>("T").property<&T::getX, &T::setX>("x");
             */
            template <auto FGet, auto FSet = nullptr>
            class_registrar &property(const char *name) {
                js_traits<std::shared_ptr<T>>::template ensureCanCastToBase<
                    FGet>(context.ctx);
                js_traits<std::shared_ptr<T>>::template ensureCanCastToBase<
                    FSet>(context.ctx);
                if constexpr (std::is_same_v<decltype(FSet), std::nullptr_t>)
                    prototype.add_getter<FGet>(name);
                else
                    prototype.add_getter_setter<FGet, FSet>(name);
                return *this;
            }

            /** Add class constructor
             * @tparam Args contructor arguments
             * @param name constructor name (if not specified class name will be
             * used)
             */
            template <typename... Args>
            class_registrar &constructor(const char *name = nullptr) {
                if (!name)
                    name = this->name;
                ctor = context.newValue(qjs::ctor_wrapper<T, Args...>{name});
                JS_SetConstructor(context.ctx, ctor.v, prototype.v);
                module.add(name, qjs::Value{ctor});
                return *this;
            }

            /** Sets the base class
             * @tparam B base class
             */
            template <class B> class_registrar &base() {
                static_assert(!std::is_same_v<B, T>,
                              "Type cannot be a base of itself");
                assert(js_traits<std::shared_ptr<B>>::QJSClassId &&
                       "base class is not registered");
                js_traits<std::shared_ptr<T>>::template ensureCanCastToBase<B>(
                    context.ctx);
                auto base_proto = JS_GetClassProto(
                    context.ctx, js_traits<std::shared_ptr<B>>::QJSClassId);
                int err = JS_SetPrototype(context.ctx, prototype.v, base_proto);
                JS_FreeValue(context.ctx, base_proto);
                if (err < 0)
                    throw exception{context.ctx};
                return *this;
            }

            /** All qjs::Value members of T should be marked by mark<> for
             * QuickJS garbage collector so that the cycle removal algorithm can
             * find the other objects referenced by this object.
             */
            template <Value T::*V> class_registrar &mark() {
                js_traits<std::shared_ptr<T>>::markOffsets.push_back(V);
                return *this;
            }

            ~class_registrar() {
                context.registerClass<T>(name, std::move(prototype));
            }
        };

    public:
        /** Add class to module.
         * See \ref class_registrar.
         */
        template <class T> class_registrar<T> class_(const char *name) {
            return class_registrar<T>{name, *this, qjs::Context::get(ctx)};
        }
    };

    std::vector<Module> modules;

private:
    void init() {
        JS_SetContextOpaque(ctx, this);
        js_traits<detail::function>::register_class(ctx, "C++ function");
    }

public:
    Context(Runtime &rt) : Context(rt.rt) {}

    Context(JSRuntime *rt) {
        ctx = JS_NewContext(rt);
        if (!ctx)
            throw std::runtime_error{"qjs: Cannot create context"};
        init();
    }

    Context(JSContext *ctx) : ctx{ctx} { init(); }

    // noncopyable
    Context(const Context &) = delete;

    ~Context() {
        // modules.clear();
        JS_FreeContext(ctx);
    }

    /** Callback triggered when a Promise rejection won't ever be handled */
    std::function<void(Value)> onUnhandledPromiseRejection;

    /** Data type returned by the moduleLoader function */
    struct ModuleData {
        std::optional<std::string> source, url;
        ModuleData() : source(std::nullopt), url(std::nullopt) {}
        ModuleData(std::optional<std::string> source)
            : source(std::move(source)), url(std::nullopt) {}
        ModuleData(std::optional<std::string> url,
                   std::optional<std::string> source)
            : source(std::move(source)), url(std::move(url)) {}
    };

    /** Function called to obtain the source of a module */
    std::function<ModuleData(std::string_view)> moduleLoader =
        [](std::string_view filename) -> ModuleData {
        return ModuleData{detail::toUri(filename), detail::readFile(filename)};
    };

    template <typename Function> void enqueueJob(Function &&job);

    /** Create module and return a reference to it */
    Module &addModule(const char *name) {
        modules.emplace_back(ctx, name);
        return modules.back();
    }

    /** returns globalThis */
    Value global() {
        return Value{weakFromContext(ctx), JS_GetGlobalObject(ctx)};
    }

    /** returns new Object() */
    Value newObject() { return Value{weakFromContext(ctx), JS_NewObject(ctx)}; }

    /** returns JS value converted from c++ object val */
    template <typename T> Value newValue(T &&val) {
        return Value{weakFromContext(ctx), std::forward<T>(val)};
    }

    /** returns current exception associated with context and clears it. Should
     * be called when qjs::exception is caught */
    Value getException() {
        return Value{weakFromContext(ctx), JS_GetException(ctx)};
    }

    /** Register class T for conversions to/from std::shared_ptr<T> to work.
     * Wherever possible module.class_<T>("T")... should be used instead.
     * @tparam T class type
     * @param name class name in JS engine
     * @param proto JS class prototype or JS_UNDEFINED
     */
    template <class T>
    void registerClass(const char *name, JSValue proto = JS_NULL) {
        js_traits<std::shared_ptr<T>>::register_class(ctx, name, proto);
    }

    /// @see JS_Eval
    Value eval(std::string_view buffer, const char *filename = "<eval>",
               int flags = 0) {
        assert(buffer.data()[buffer.size()] == '\0' &&
               "eval buffer is not null-terminated"); // JS_Eval requirement
        JSValue v = JS_Eval(ctx, buffer.data(), buffer.size(), filename, flags);
        return Value{weakFromContext(ctx), std::move(v)};
    }

    /// @see JS_Eval
    Value evalThis(std::string_view buffer, const char *filename = "<eval>",
                   int flags = 0, Value this_obj = JS_UNDEFINED) {
        assert(buffer.data()[buffer.size()] == '\0' &&
               "eval buffer is not null-terminated"); // JS_Eval requirement
        JSValue v = JS_EvalThis(ctx, this_obj.v, buffer.data(), buffer.size(),
                                filename, flags);
        return Value{weakFromContext(ctx), std::move(v)};
    }

    Value evalFile(const char *filename, int flags = 0) {
        auto buf = detail::readFile(filename);
        if (!buf)
            throw std::runtime_error{
                std::string{"evalFile: can't read file: "} + filename};
        return eval(*buf, filename, flags);
    }

    /// @see JS_ParseJSON2
    Value fromJSON(std::string_view buffer, const char *filename = "<fromJSON>",
                   int flags = 0) {
        assert(buffer.data()[buffer.size()] == '\0' &&
               "fromJSON buffer is not null-terminated"); // JS_ParseJSON
                                                          // requirement
        return Value{weakFromContext(ctx),
                     JS_ParseJSON(ctx, buffer.data(), buffer.size(), filename)};
    }

    /** Get qjs::Context from JSContext opaque pointer */
    static Context &get(JSContext *ctx) {
        void *ptr = JS_GetContextOpaque(ctx);
        assert(ptr);
        return *static_cast<Context *>(ptr);
    }
};

/** Conversion traits for Value.
 */
template <> struct js_traits<Value> {
    static Value unwrap(JSContext *ctx, JSValueConst v) {
        return Value{weakFromContext(ctx), JS_DupValue(ctx, v)};
    }

    static JSValue wrap(JSContext *ctx, Value v) noexcept {
        assert(ctx);
        assert(JS_GetRuntime(ctx) == JS_GetRuntime(v.ctx));
        return v.release();
    }
};

struct qjs_context_destroyed_exception : public std::exception {
    const char *what() const noexcept override {
        return "qjs::Context is destroyed";
    }
};

/** Convert to/from std::function. Actually accepts/returns callable object that
 * is compatible with function<R (Args...)>.
 * @tparam R return type
 * @tparam Args argument types
 */
template <typename R, typename... Args>
struct js_traits<std::function<R(Args...)>, int> {
    static auto unwrap(JSContext *ctx, JSValueConst fun_obj) {
        auto weak = Context::get(ctx).weak_from_this();
        return
            [jsfun_obj = Value{weakFromContext(ctx), JS_DupValue(ctx, fun_obj)},
             weak](Args... args) -> R {
                if (weak.expired())
                    throw qjs_context_destroyed_exception{};
                auto &ctx = Context::get(jsfun_obj.ctx);
                std::promise<std::expected<JSValue, exception>> promise;
                auto future = promise.get_future();

                auto work = [&]() {
                    const int argc = sizeof...(Args);
                    JSValue argv[std::max(1, argc)];
                    detail::wrap_args(jsfun_obj.ctx, argv,
                                      std::forward<Args>(args)...);
                    JSValue result =
                        JS_Call(jsfun_obj.ctx, jsfun_obj.v, JS_UNDEFINED, argc,
                                const_cast<JSValueConst *>(argv));
                    for (int i = 0; i < argc; i++)
                        JS_FreeValue(jsfun_obj.ctx, argv[i]);
                    promise.set_value(result);

                    if (JS_IsException(result))
                        promise.set_exception(
                            std::make_exception_ptr(exception{jsfun_obj.ctx}));
                };

                if (!is_thread_js_main) {
                    ctx.enqueueJob(work);
                } else {
                    work();
                }

                wait_with_msgloop([&future]() { future.wait(); });
                auto result = future.get();
                if (!result)
                    throw result.error();

                return detail::unwrap_free<R>(jsfun_obj.ctx, result.value());
            };
    }

    /** Convert from function object functor to JSValue.
     * Uses detail::function for type-erasure.
     */
    template <typename Functor>
    static JSValue wrap(JSContext *ctx, Functor &&functor) noexcept {
        using detail::function;
        assert(js_traits<function>::QJSClassId);
        auto obj = JS_NewObjectClass(ctx, js_traits<function>::QJSClassId);
        if (JS_IsException(obj))
            return obj;
        try {
            auto fptr = function::create(JS_GetRuntime(ctx),
                                         std::forward<Functor>(functor));
            fptr->invoker = [](function *self, JSContext *ctx,
                               JSValueConst this_value, int argc,
                               JSValueConst *argv) {
                assert(self);
                auto f =
                    reinterpret_cast<std::decay_t<Functor> *>(&self->functor);
                return detail::wrap_call<R, Args...>(ctx, *f, argc, argv);
            };
            JS_SetOpaque(obj, fptr);
            return obj;
        } catch (const std::exception &e) {
            JS_ThrowInternalError(ctx, "%s", e.what());
            return JS_EXCEPTION;
        } catch (...) {
            JS_ThrowInternalError(ctx, "Unknown errror");
            return JS_EXCEPTION;
        }
    }
};

namespace detail {

template <typename T, typename = void>
struct is_callable : std::is_function<T> {};

template <typename T>
struct is_callable<
    T, std::enable_if_t<std::is_same_v<decltype(void(&T::operator())), void>>>
    : std::true_type {};

template <typename T>
inline constexpr bool is_callable_v = is_callable<T>::value;

} // namespace detail

template <typename Function>
struct js_traits<Function, std::enable_if_t<detail::is_callable_v<Function>>> {
    static auto unwrap(JSContext *ctx, JSValueConst fun_obj) {
        return js_traits<decltype(std::function{std::declval<Function>()}),
                         int>::unwrap(ctx, fun_obj);
    }

    template <typename Functor>
    static JSValue wrap(JSContext *ctx, Functor &&functor) {
        return js_traits<decltype(std::function{std::declval<Function>()}),
                         int>::wrap(ctx, std::forward<Functor>(functor));
    }
};

/** Convert from std::vector<T> to Array and vice-versa. If Array holds objects
 * that are non-convertible to T throws qjs::exception */
template <class T> struct js_traits<std::vector<T>> {
    static JSValue wrap(JSContext *ctx, const std::vector<T> &arr) noexcept {
        try {
            auto jsarray = Value{weakFromContext(ctx), JS_NewArray(ctx)};
            for (uint32_t i = 0; i < (uint32_t)arr.size(); i++)
                jsarray[i] = arr[i];
            return jsarray.release();
        } catch (exception) {
            return JS_EXCEPTION;
        } catch (std::exception const &err) {
            JS_ThrowInternalError(ctx, "%s", err.what());
            return JS_EXCEPTION;
        } catch (...) {
            JS_ThrowInternalError(ctx, "Unknown error");
            return JS_EXCEPTION;
        }
    }

    static std::vector<T> unwrap(JSContext *ctx, JSValueConst jsarr) {
        int e = JS_IsArray(jsarr);
        if (e == 0)
            JS_ThrowTypeError(
                ctx, "js_traits<std::vector<T>>::unwrap expects array");
        if (e <= 0)
            throw exception{ctx};
        Value jsarray{ctx, JS_DupValue(ctx, jsarr)};
        std::vector<T> arr;
        auto len = static_cast<int32_t>(jsarray["length"]);
        arr.reserve((uint32_t)len);
        for (uint32_t i = 0; i < (uint32_t)len; i++)
            arr.push_back(static_cast<T>(jsarray[i]));
        return arr;
    }
};

template <typename U, typename V> struct js_traits<std::pair<U, V>> {
    static JSValue wrap(JSContext *ctx, std::pair<U, V> obj) noexcept {
        try {
            auto jsarray = Value{weakFromContext(ctx), JS_NewArray(ctx)};
            jsarray[uint32_t(0)] = std::move(obj.first);
            jsarray[uint32_t(1)] = std::move(obj.second);
            return jsarray.release();
        } catch (exception) {
            return JS_EXCEPTION;
        } catch (std::exception const &err) {
            JS_ThrowInternalError(ctx, "%s", err.what());
            return JS_EXCEPTION;
        } catch (...) {
            JS_ThrowInternalError(ctx, "Unknown error");
            return JS_EXCEPTION;
        }
    }

    static std::pair<U, V> unwrap(JSContext *ctx, JSValueConst jsarr) {
        int e = JS_IsArray(jsarr);
        if (e == 0)
            JS_ThrowTypeError(ctx, "js_traits<%s>::unwrap expects array",
                              QJSPP_TYPENAME(std::pair<U, V>));
        if (e <= 0)
            throw exception{ctx};
        Value jsarray{ctx, JS_DupValue(ctx, jsarr)};
        const auto len = static_cast<uint32_t>(jsarray["length"]);
        if (len != 2) {
            JS_ThrowTypeError(ctx,
                              "js_traits<%s>::unwrap expected array of length "
                              "2, got length %d",
                              QJSPP_TYPENAME(std::pair<U, V>), len);
            throw exception{ctx};
        }
        return std::pair<U, V>{static_cast<U>(jsarray[uint32_t(0)]),
                               static_cast<V>(jsarray[uint32_t(1)])};
    }
};

template <typename... T> struct js_traits<std::tuple<T...>> {
    static JSValue wrap(JSContext *ctx, const std::tuple<T...> &obj) noexcept {
        try {
            auto jsarray = Value{weakFromContext(ctx), JS_NewArray(ctx)};

            std::apply(
                [&jsarray, ctx](const auto &...args) {
                    ((jsarray[uint32_t(args)] =
                          static_cast<std::decay_t<decltype(args)>>(args)),
                     ...);
                },
                obj);
            return jsarray.release();
        } catch (exception) {
            return JS_EXCEPTION;
        } catch (std::exception const &err) {
            JS_ThrowInternalError(ctx, "%s", err.what());
            return JS_EXCEPTION;
        } catch (...) {
            JS_ThrowInternalError(ctx, "Unknown error");
            return JS_EXCEPTION;
        }
    }
    static std::tuple<T...> unwrap(JSContext *ctx, JSValueConst jsarr) {
        if (!JS_IsArray(jsarr)) {
            JS_ThrowTypeError(ctx, "Expected an array");
            throw exception{ctx};
        }

        int32_t len;
        if (JS_ToInt32(ctx, &len, JS_GetPropertyStr(ctx, jsarr, "length")) <
            0) {
            throw exception{ctx};
        }

        constexpr size_t tuple_size = sizeof...(T);
        if (static_cast<size_t>(len) != tuple_size) {
            JS_ThrowRangeError(ctx,
                               "Array length %d does not match tuple size %zu",
                               len, tuple_size);
            throw exception{ctx};
        }

        return unwrap_impl(ctx, jsarr, std::index_sequence_for<T...>{});
    }

private:
    template <size_t... I>
    static std::tuple<T...> unwrap_impl(JSContext *ctx, JSValueConst jsarr,
                                        std::index_sequence<I...>) {
        try {
            return std::make_tuple(
                js_traits<std::tuple_element_t<I, std::tuple<T...>>>::unwrap(
                    ctx, JS_GetPropertyUint32(ctx, jsarr, I))...);
        } catch (...) {
            JS_ThrowInternalError(ctx,
                                  "Failed to unwrap tuple element at index %zu",
                                  sizeof...(I) - 1);
            throw;
        }
    }
};

/** Conversions for std::optional.
 * Unlike other types does not throw on unwrap but returns nullopt.
 * Converts std::nullopt to null.
 */
template <typename T> struct js_traits<std::optional<T>> {
    /** Wraps T or null. */
    static JSValue wrap(JSContext *ctx, std::optional<T> obj) noexcept {
        if (obj)
            return js_traits<std::decay_t<T>>::wrap(ctx, *obj);
        return JS_NULL;
    }

    /** If conversion to T fails returns std::nullopt. */
    static auto unwrap(JSContext *ctx, JSValueConst v) noexcept
        -> std::optional<decltype(js_traits<std::decay_t<T>>::unwrap(ctx, v))> {
        try {
            if (JS_IsNull(v) || JS_IsUndefined(v))
                return std::nullopt;
            return js_traits<std::decay_t<T>>::unwrap(ctx, v);
        } catch (std::exception &e) {
            // ignore and clear exception
            JS_FreeValue(ctx, JS_GetException(ctx));
        }
        return std::nullopt;
    }
};

namespace detail {
template <typename Key> property_proxy<Key>::operator Value() const {
    return as<Value>();
}
} // namespace detail

template <typename Function> void Context::enqueueJob(Function &&job) {
    std::lock_guard<std::mutex> lock(js_job_start_mutex);

    JSValue job_val = js_traits<std::function<void()>>::wrap(
        ctx, std::forward<Function>(job));
    JSValueConst arg = job_val;
    int err = JS_EnqueueJob(
        ctx,
        [](JSContext *ctx, int argc, JSValueConst *argv) {
            try {
                assert(argc >= 1);
                js_traits<std::function<void()>>::unwrap(ctx, argv[0])();
            } catch (exception) {
                return JS_EXCEPTION;
            } catch (std::exception const &err) {
                JS_ThrowInternalError(ctx, "%s", err.what());
                return JS_EXCEPTION;
            } catch (...) {
                JS_ThrowInternalError(ctx, "Unknown error");
                return JS_EXCEPTION;
            }
            return JS_UNDEFINED;
        },
        1, &arg);

    has_pending_job = true;
    js_job_start_cv.notify_all();
    JS_FreeValue(ctx, job_val);
    if (err < 0)
        throw exception{ctx};
}

inline Context &exception::context() const { return Context::get(ctx); }

inline Value exception::get() { return context().getException(); }

inline void Runtime::promise_unhandled_rejection_tracker(JSContext *ctx,
                                                         JSValue promise,
                                                         JSValue reason,
                                                         bool is_handled,
                                                         void *opaque) {
    auto &context = Context::get(ctx);
    if (context.onUnhandledPromiseRejection) {
        // context.onUnhandledPromiseRejection(
        //     context.newValue(JS_DupValue(ctx, reason)));
    }
}

inline JSModuleDef *
Runtime::module_loader(JSContext *ctx, const char *module_name, void *opaque) {
    Context::ModuleData data;
    auto &context = Context::get(ctx);

    try {
        if (context.moduleLoader)
            data = context.moduleLoader(module_name);

        if (!data.source) {
            JS_ThrowReferenceError(ctx, "could not load module filename '%s'",
                                   module_name);
            return NULL;
        }

        if (!data.url)
            data.url = module_name;

        // compile the module
        auto func_val =
            context.eval(*data.source, module_name,
                         JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY);
        assert(JS_VALUE_GET_TAG(func_val.v) == JS_TAG_MODULE);
        JSModuleDef *m =
            reinterpret_cast<JSModuleDef *>(JS_VALUE_GET_PTR(func_val.v));

        // set import.meta
        auto meta = context.newValue(JS_GetImportMeta(ctx, m));
        meta["url"] = *data.url;
        meta["main"] = false;

        return m;
    } catch (exception) {
        return NULL;
    } catch (std::exception const &err) {
        JS_ThrowInternalError(ctx, "%s", err.what());
        return NULL;
    } catch (...) {
        JS_ThrowInternalError(ctx, "Unknown error");
        return NULL;
    }
}

inline Context *Runtime::executePendingJob() {
    JSContext *ctx;
    auto err = JS_ExecutePendingJob(rt, &ctx);
    if (err == 0) {
        // There was no job to run
        return nullptr;
    } else if (err < 0) {
        throw exception{ctx};
    }
    return &Context::get(ctx);
}

inline std::string exception::message(JSContext *ctx) const {
    auto &js = Context::get(ctx ? ctx : this->ctx);
    auto exc = js.getException();

    std::string message = (std::string)exc;
    if ((bool)exc["stack"])
        message += "\n" + (std::string)exc["stack"];

    return message;
}

inline void setCurrentContext(JSContext *ctx) {
    Context::current = &Context::get(ctx);
}

inline JSContext *getContextFromWrapped(Context *ctx) { return ctx->ctx; }

inline std::weak_ptr<Context> weakFromContext(JSContext *ctx) {
    return Context::get(ctx).weak_from_this();
}
} // namespace qjs
